package com.maikang.exammall.service;

import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.service.WxPayService;
import com.maikang.exammall.entity.Package;
import com.maikang.exammall.entity.*;
import com.maikang.exammall.entity.constant.BillStatus;
import com.maikang.exammall.entity.constant.FeeType;
import com.maikang.exammall.entity.constant.SaleRole;
import com.maikang.exammall.entity.constant.TransType;
import com.maikang.exammall.entity.dto.order.*;
import com.maikang.exammall.exception.BadRequestException;
import com.maikang.exammall.repository.*;
import com.maikang.exammall.task.async.CartAsyncTask;
import com.maikang.exammall.task.async.CommissionAsyncTask;
import com.maikang.exammall.task.async.PackageAsyncTask;
import com.maikang.exammall.task.async.WechatMessageTemplate;
import com.maikang.exammall.util.DateUtil;
import com.maikang.exammall.util.IDUtil;
import com.maikang.exammall.util.WeChatUtil;
import com.maikang.exammall.util.idvalidator.IDValidator;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.validation.Valid;
import java.util.*;

import static com.maikang.exammall.entity.constant.OrderStatus.*;
import static com.maikang.exammall.entity.constant.PackageTargetConstant.*;

@Log4j2
@Service
@RequiredArgsConstructor
public class OrderService extends BaseService<Order, Long, OrderRepository> {
    private final PackageRepository pkgRepository;
    private final BillRepository billRepository;
    private final WxPayService wxPayService;
    private final WeChatUtil wechatUtil;
    private final WechatMessageTemplate wechatMessageTemplate;
    private final PackageAsyncTask packageAsyncTask;
    private final CartAsyncTask cartAsyncTask;
    private final SaleRepository saleRepository;
    private final CommissionAsyncTask commissionAsyncTask;
    private final CommissionRepository commissionRepository;
    private final OrderItemRepository orderItemRepository;
    private IDValidator validator = new IDValidator();

    @Transactional(rollbackFor = Exception.class)
    public WxPayMpOrderResult launchSecondPay(String outTradeNo) {
        Bill bill = billRepository.findByOutTradeNoAndTransType(outTradeNo, TransType.INCOMES);
        if (bill == null || !bill.getStatus().equals(BillStatus.TO_PAY)
                || !StringUtils.hasLength(bill.getNonceStr())
                || !StringUtils.hasLength(bill.getPrepayId())
                || bill.getTimeExpire().before(new Date())){
            throw new BadRequestException("找不到该订单/该订单已不在待支付状态/已过了支付的期限");
        }
        return wechatUtil.launchSecondPay(bill.getNonceStr(), bill.getPrepayId());
    }


    @Transactional(rollbackFor = Exception.class)
    public CreateOrderOutputDTO createOrder(CreateOrderInputDTO orderInput) {
        User user = UserContext.getUser();
        log.info("createOrder, user: {}", user);
        Long userId = UserContext.getUserId();
        Sale sale = saleRepository.findBySaleId(userId);
        if (sale == null && user.getRefererId() != 0) {
            sale = saleRepository.findBySaleId(user.getRefererId());
        }
        log.info("createOrder, sale: {}", sale);

        List<OrderItem> orderItems = orderInput.getOrderItems();

        Map<Long, Package> map = new HashMap<>();
        Set<Long> pkgIds = new HashSet<>();
        Set<Long> hospitalIds = new HashSet<>();
        orderItems.forEach(orderItem -> pkgIds.add(orderItem.getPkgId()));

        List<Package> pkgs = pkgRepository.findAllById(pkgIds);
        assertPackagesExist(pkgIds, pkgs);

        pkgs.forEach(pkg -> {
            map.put(pkg.getId(), pkg);
            hospitalIds.add(pkg.getHospital().getId());
        });

        assertHospitalCountEqualsOne(hospitalIds);

        // TODO 增加立即下单数，日后可改成消息队列
        packageAsyncTask.incrementPackageOrderCount(pkgIds);
        packageAsyncTask.incrementHospitalOrderCount(hospitalIds);

        List<Commission> commissions = new ArrayList<>();
        Long amount = 0L;
        for (OrderItem orderItem : orderItems) {
            // 参数校验
            validatePkgTargetAndExamineeGender(map, orderItem);
            validateExamDate(map, orderItem);
            // 下单相关
            orderItem.setUserId(userId);
            Long priceReal = map.get(orderItem.getPkgId()).getPriceReal();
            orderItem.setPkgPrice(priceReal);
            orderItem.setPkgName(map.get(orderItem.getPkgId()).getName());
            orderItem.setHospitalName(map.get(orderItem.getPkgId()).getHospital().getName());
            orderItem.setHospitalId(map.get(orderItem.getPkgId()).getHospital().getId());
            orderItem.setHospitalAvatarUrl(map.get(orderItem.getPkgId()).getHospital().getAvatarUrl());
            orderItem.setDistributableRate(map.get(orderItem.getPkgId()).getDistributableRate());
            orderItem.setFirstSaleRate(map.get(orderItem.getPkgId()).getFirstSaleRate());
            orderItem.setManageRate(map.get(orderItem.getPkgId()).getManageRate());
            orderItem.setSecondSaleRate(map.get(orderItem.getPkgId()).getSecondSaleRate());
            amount += priceReal;
        }
        Order order = Order.builder().outTradeNo(IDUtil.createOrderOutTradeNo())
                .userId(userId)
                .mobile(orderInput.getMobile())
                .openId(UserContext.getUser().getOpenId())
                .amount(amount)
                .status(TO_PAY)
                .remark(orderInput.getRemark()).build();
        // 方法一： 非cascade 添加, 只需要子表设置好关系, 父表不设置关系, 调用2个save.
//        order.setOrderItems(orderItems); // mappedBy 父方 放弃维护权， 不能够update 子方的 parentId
//        orderItems.forEach(orderItem -> orderItem.setOrder(order));
//        Order save = repository.save(order);
//        List<OrderItem> saveAll = orderItemRepository.saveAll(orderItems);


        // 方法2： cascade 添加;  双方设置完关系后, 保存父表即可
        order.setOrderItems(orderItems); // mappedBy 父方 放弃维护权， 不能够update 子方的 parentId
        orderItems.forEach(orderItem -> orderItem.setOrder(order));
        Order savedOrder = repository.save(order);
        log.info("createOrder, savedOrder: {}", savedOrder);

        WxPayMpOrderResult prepayOrderResult = this.makeWechatOrderAndPrepay(savedOrder, orderInput.getIp());

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.MINUTE, 30);
        // 创建 bill
        Bill bill = Bill.builder().orderId(order.getId())
                .outTradeNo(order.getOutTradeNo())
                .prepayId(prepayOrderResult.getPackageValue().substring(10)) // sample packageValue: prepay_id=wx19125100210041b47b157a2453098b0000
                .nonceStr(prepayOrderResult.getNonceStr())
                .totalFee(order.getAmount())
                .feeType(FeeType.INCOMES)
                .status(TO_PAY)
                .transType(TransType.INCOMES)
                .timeExpire(calendar.getTime())
                .build();
        Bill savedBill = billRepository.save(bill);
        log.info("payment bill created: {}", savedBill);

        // 地推相关
        for (OrderItem orderItem : savedOrder.getOrderItems()) {
            if (sale != null) {
                if (sale.getRole().equals(SaleRole.FIRST_LEVEL)) { // 只有自己有佣金
                    commissions.add(new Commission(orderItem, savedOrder.getId(), sale.getSaleId(), orderItem.getFirstLevelSaleCommission()));
                } else if (sale.getRole().equals(SaleRole.SECOND_LEVEL)) {
                    // 自己有佣金
                    commissions.add(new Commission(orderItem, savedOrder.getId(), sale.getSaleId(), orderItem.getSecondLevelSaleCommission()));
                    // 上级有管理费
                    if (sale.getLeaderId() != 0) {
                        commissions.add(new Commission(orderItem, savedOrder.getId(), sale.getLeaderId(), orderItem.getManageCommission()));
                    }
                }
            }
        }
        commissionRepository.saveAll(commissions);
        cartAsyncTask.deleteCartsByIds(orderInput.getCartIds());
        return CreateOrderOutputDTO.builder().wxPayMpOrderResult(prepayOrderResult).order(savedOrder).build();
    }

    /**
     * 返回前台H5调用JS支付所需要的参数，公众号支付调用此接口
     */
    @SneakyThrows
    private WxPayMpOrderResult makeWechatOrderAndPrepay(Order order, String ip) {
        WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
                .openid(order.getOpenId())
                .outTradeNo(order.getOutTradeNo())
                .totalFee(order.getAmount().intValue())
                .body("迈康-体检套餐")
                .tradeType("JSAPI")
                .spbillCreateIp(ip)
                .notifyUrl(wxPayService.getConfig().getKeyPath()) // keyPath is notifyUrl
                .build();

        WxPayMpOrderResult prepayOrderResult = wxPayService.createOrder(request);
        log.info("order: {} 前台H5调用JS支付所需要的参数: {}", order, prepayOrderResult);
        return prepayOrderResult;

    }

    private void assertPackagesExist(Set<Long> pkgIds, List<Package> pkgs) {
        // 校验套餐是否存在
        if (ObjectUtils.isEmpty(pkgs) || pkgs.size() < pkgIds.size()) {
            throw new BadRequestException("下单的套餐id列表中某些套餐不存在");
        }
    }

    private void assertHospitalCountEqualsOne(Set<Long> hospitalIds) {
        // 校验一个订单中是否包含两个以上的医院
        if (hospitalIds.size() > 1) {
            throw new BadRequestException("暂不支持跨医院/体检机构下单");
        }
    }

    private void validateExamDate(Map<Long, Package> map, OrderItem orderItem) {
        Date examineDate = orderItem.getExamineDate();
        if (examineDate.before(DateUtil.tomorrow())) {
            throw new BadRequestException("需至少提前一天预约体检");
        }
        Set<Integer> openDates = map.get(orderItem.getPkgId()).getHospital().getOpenDates();
        if (!openDates.contains(DateUtil.getWeekDay(examineDate))) {
            throw new BadRequestException("该医院在这天不开放体检， 请重新选择体检日期");
        }
    }

    private void validatePkgTargetAndExamineeGender(Map<Long, Package> map, OrderItem orderItem) {
        // 校验pkgTarget 是否和体检人 examineeGender 相符合
        Integer targetGender = map.get(orderItem.getPkgId()).getTarget();
        int examineeGender = validator.getInfo(orderItem.getIdCardNo()).getGender();
        if (!targetGender.equals(ANY_GENDER)) {
            if (targetGender.equals(MALE) && examineeGender != MALE) {
                throw new BadRequestException("此为'男性'套餐，女性人员是无法体检的，请悉知");
            } else if ((targetGender.equals(UN_MARRIED_FEMALE) || targetGender.equals(MARRIED_FEMALE)) && examineeGender != FEMALE) {
                throw new BadRequestException("此为'女性'套餐，男性人员是无法体检的，请悉知");
            }
        }
        orderItem.setGender(examineeGender);
    }

    public Iterable<OrderDetailDTO> findAll(SearchOrderInputDTO inputDTO, Pageable pageable) {

        Specification<Order> specification = (Specification<Order>) (root, query, cb) -> {
            List<Predicate> predicatesList = new ArrayList<>();
            predicatesList.add(cb.equal(root.get(Order_.userId), UserContext.getUserId()));
            predicatesList.add(cb.equal(root.get(Order_.invisibleToUser), false));

            if (inputDTO.getStatus() != null) {
                predicatesList.add(cb.equal(root.get(Order_.status), inputDTO.getStatus()));
            }
            Join<Order, OrderItem> joinOrderItem = (Join<Order, OrderItem>) root.fetch(Order_.orderItems, JoinType.INNER);

            if (StringUtils.hasLength(inputDTO.getName())) {
                String name = '%' + inputDTO.getName() + '%';
                Predicate pkgName = cb.like(joinOrderItem.get("pkgName"), name);
                Predicate hospitalName = cb.like(joinOrderItem.get("hospitalName"), name);
                Predicate namePredicate = cb.or(pkgName, hospitalName);
                predicatesList.add(namePredicate);
            }
            Predicate[] predicates = new Predicate[predicatesList.size()];
            query.orderBy(cb.desc(root.get(Order_.createdDate)));
            return cb.and(predicatesList.toArray(predicates));
        };

        Slice<Order> all = repository.findAllSliced(specification, pageable);
        // serialization
        List<OrderDetailDTO> orderDetailDTOS = OrderDetailDTO.ofOrderList(all.getContent());
        SliceImpl<OrderDetailDTO> slice = new SliceImpl(orderDetailDTOS, all.getPageable(), all.hasNext());
        return slice;
    }

    public OrderDetailDTO findByIdAndUserId(Long id, Long userId) {
        Order order = repository.findByIdAndUserIdAndInvisibleToUser(id, userId, false);
        if (order == null) return null;
        return new OrderDetailDTO(order);
    }

    @Transactional(rollbackFor = Exception.class)
    public Order applyOrderRefund(Long id, @Valid RefundOrderInputDTO input) {
        Order orderInDB = repository.findByIdAndUserIdAndInvisibleToUser(id, UserContext.getUserId(), false);
        if (orderInDB==null) {
            throw new BadRequestException("该用户名下找不到该订单");
        }
        if (!orderInDB.getStatus().equals(PAID)) {
            throw new BadRequestException("该订单" + id + "不属于已支付状态，不能申请退款，如有疑问请联系客服");
        }
        List<OrderItem> orderItems = orderInDB.getOrderItems();
        Map<Long, OrderItem> orderItemMapInDB = new HashMap<> ();
        orderItems.forEach(item -> orderItemMapInDB.put(item.getId(), item));
        List<Long> itemIdsReadyToRefund = new ArrayList<>();
        input.getOrderItemRefundBriefs().forEach(item -> {
            if (!orderItemMapInDB.containsKey(item.getId()) || !id.equals(item.getOrderId())) {
                throw new BadRequestException("orderItemId: " + item.getId() + "不属于该订单:" + id + ", 或者该orderItem 的orderId" + item.getOrderId() + "与所要修改的订单id不一致");
            }
            if (!orderItemMapInDB.get(item.getId()).getStatus().equals(PAID)) {
                throw new BadRequestException("订单项目: " + item.getId() + "不属于可申请退款的状态");
            }
            if (DateUtil.truncateToZeroOclock(orderItemMapInDB.get(item.getId()).getExamineDate()).before(new Date())) {
                throw new BadRequestException("订单项目: " + item.getId() + "已超过可退款的时间范围");
            }
            orderItemMapInDB.get(item.getId()).setRefundReasonId(item.getRefundReasonId());
            orderItemMapInDB.get(item.getId()).setRefundReasonRemark(item.getRefundReasonRemark());
            orderItemMapInDB.get(item.getId()).setStatus(APPLYING_FOR_REFUND);
            // 地推相关
            itemIdsReadyToRefund.add(item.getId());
        });

        ArrayList<OrderItem> orderItems1 = new ArrayList<>(orderItemMapInDB.values());
        orderInDB.setStatus(APPLYING_FOR_REFUND);
        orderItems1.forEach(orderItem -> orderItem.setOrder(orderInDB));
        orderInDB.setOrderItems(orderItems1);
        Order save = repository.save(orderInDB);

        // 地推相关
        commissionRepository.updateStatusByOrderItemIds(itemIdsReadyToRefund, APPLYING_FOR_REFUND);
        // 发送短信通知后台人员处理。
        wechatMessageTemplate.NotifyOperatorsAfterOrderRefundIsApplied(save);
        return save;
    }

    @Transactional(rollbackFor = Exception.class)
    public Order cancelOrder(Long id, Order entity) {
        Optional<Order> targetOptional = repository.findByIdAndUserIdAndInvisibleToUserAndStatus(id, UserContext.getUserId(), false, TO_PAY);
        if (!targetOptional.isPresent()) {
            throw new BadRequestException("该订单" + id + "不处于可取消的状态");
        }
        Order order = targetOptional.get();
        order.setStatus(CLOSED);
        order.setCancelReasonId(entity.getCancelReasonId());
        Order save = repository.save(order);
        orderItemRepository.updateStatusByOrderIdAndStatus(TO_PAY, CLOSED, Collections.singletonList(id));
        // 地推相关
        commissionRepository.updateStatusByOrderId(id, CLOSED);
        return save;
    }

    public Integer pollPayStatus(String outTradeNo) {
        return billRepository.findStatusByOutTradeNoAndTransType(outTradeNo, TransType.INCOMES);
    }

    public Integer countByStatus(Integer status) {
        return repository.countByStatusAndUserId(status, UserContext.getUserId());
    }
}
